package searcherPackage;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;
import searcherPackage.Ranking.BM25Ranker;
import searcherPackage.Index.Index;
import searcherPackage.Index.InvalidTermException;
import searcherPackage.Index.PostingsList;
import searcherPackage.Index.Term;
import util.collectionStats.CollectionStatsRegistry;
import util.collectionStats.CollectionStatsRegistry.Stats;
import util.docsCache.CachedDocument;

/**
 * @author i_khri
 */
public class Searcher {
    /* DEBUG */
    private static final boolean DEBUG = false;
    /* ***** */

    public static Searcher newInstance(String indexLocation, String cacheLocation){
        return new Searcher(indexLocation, cacheLocation);
    }



    public void runSearch(){
        String query = " ";

        do{
            welcome();
            query = getQuery();

            // terms are lexicographically ordered to optimize term-postings list mapping.
            Set<String> queryTerms = QueryPreProcessor.extractTerms(query);
            query = QueryPreProcessor.preProcessQuery(query);


            /* DEBUG */
            if(DEBUG){
                System.out.println("FROM QUERY [" +query+ "] EXTRACTED TERMS " + queryTerms);
                System.out.println();
                System.out.println("new query: " + query);
            }
            /* ***** */



            /* mapping tokens to postings lists */
            Index termsMap = null;
            try{
                termsMap = mapTerms(queryTerms);

                /* DEBUG */
                if(DEBUG){
                    System.out.println("Terms map: ");
                    for(String term : queryTerms){
                        Term t = null;
                        try {
                            t = Term.newInstance(term);
                            PostingsList pl = termsMap.getPostingsList(t);
                            System.out.println(t + " --> " + pl);
                        } catch (InvalidTermException ex) {
                            System.err.println(ex.getMessage());
                        }
                    }
                }
                /* ***** */

            }catch (FileNotFoundException e){
                System.out.println("Internal error. Index could not be found in " + indexLocation + ". Terminating.");
                System.exit(0);
            }catch (IOException e){
                System.out.println("Internal error. Index ended unexpectedly. Terminating.");
                System.exit(0);
            }


            /* computing query, ranking and displaying results */
            PostingsList rawResults = computeQuery(query, termsMap);
            displayResultStats(rawResults);
            List<Integer> rankedResults = rankResults(termsMap, rawResults);
            displayResult(rankedResults);
        }while(searchAgain());

        bye();
    }



    /* ------------------------------ *
     *        Auxilary methods        *
     * ------------------------------ */



    private void displayResultStats(PostingsList result){
        System.out.println();
        System.out.print("Found total " + result.size() + " results");

//        if(!result.isEmpty()){
//            System.out.println(" in documents with the following ids: " + result);
//            System.out.println();
//        }
        System.out.println();
    }



    private List<Integer> rankResults(Index termsMapping, PostingsList results){
        List<Integer> ranked = new ArrayList<Integer>();
        try{
            CollectionStatsRegistry statsRegistry = CollectionStatsRegistry.statsRegistryFromFile(cacheLocation+"\\stats.registry");
            int numDocs = statsRegistry.getInt(Stats.TOTAL_DOCS);
            double avgDocLength = statsRegistry.getDouble(Stats.AVG_DOC_LENGTH);
            HashMap<Integer, Integer> docsLengths = (HashMap<Integer, Integer>) statsRegistry.getObject(Stats.DOCS_LENGHTS);

            System.out.println("Ranking...");

            // Not using DocPositions for document lengths because along with it much extra information will be passed
            // to the ranker. The ranker will have to parse DocPositions and thus it must have knowledge about things
            // absolutely unrelated to ranking, something it is not intended to do. Moreover, should structure of
            // DocPositions ever change, ranker will be affected as well.
            BM25Ranker ranker = BM25Ranker.newInstance(termsMapping, results, numDocs, avgDocLength, docsLengths);
            ranker.setDfScale(0.7); // k1
            ranker.setDocLengthScale(1); // b
            ranked = ranker.rank();
            System.out.println("Ranked results: " + ranked);
        }catch(Exception e){
            System.err.println(e.getMessage());
            System.err.println("Cause: " + e.getCause().getMessage());
            System.out.println("Could not retrieve collection's statistics.\nRanking will be skipped.");
            for(PostingsList.Entry entry : results){
                ranked.add(entry.getDocId());
            }
        }
        return ranked;
    }



    /**
     * Displays the final postings list.
     */
    private void displayResult(List<Integer> result){
        int i = 0;
        if(!result.isEmpty()){
            if(MAX_DOCS > 0){
                System.out.println();
                System.out.println();
                System.out.println("Retrieving...");
                System.out.println();
                System.out.println();
            }
            for(Integer entry : result){
                if(++i > MAX_DOCS){
                    break;
                }
                displayDocumentFromCache(entry); // displaying the actual document.

                System.out.println();
                System.out.println();
                System.out.println();
                System.out.println();
            }
        }
        System.out.println();
        System.out.print("Total "+ result.size() +" results");
        System.out.println();
        System.out.println();
    }



    private void displayDocumentFromCache(int id){
        BufferedReader reader;
        String line = "";
        String cacheFile = cacheLocation + "\\doc-positions.pos";
        String collectionFile = "";
        CachedDocument cachedDoc = null;

        try{
            reader = new BufferedReader(new FileReader(new File(cacheFile)));
            while((line = reader.readLine()) != null){
                CachedDocument cd = CachedDocument.forCacheEntry(line, cacheLocation);
                int docId = cd.getId();
                if(docId == id){
                    cachedDoc = cd;
                    break;
                }
            }
        }catch(FileNotFoundException e){
            System.out.println("Cache file could not be found in " + cacheFile + ". Terminating.");
            return;
        }catch(IOException e){
            System.out.println("Cache file " + cacheFile + " ended unexpectedly. Terminating.");
            return;
        }catch(NumberFormatException e){
            System.out.println(line.split("=")[0] + " is not valid document id.");
            return;
        }


        if(cachedDoc == null){
            System.out.println("Document with id " + id + " could not be located.");
            return;
        }

        try{
            System.out.println(cachedDoc.getContents());
        }catch(FileNotFoundException e){
            System.out.println("Collection file could not be found in " + collectionFile + ". Terminating.");
            System.exit(0);
        }catch (IOException e){
            System.out.println("Collection file " + collectionFile + " ended unexpectedly. Terminating.");
            System.exit(0);
        }
    }



    /**
     * Evaluates postings list which corresponds to the boolean query <b>query</b>
     * @param query query.
     * @param termsMap mapping of terms extracted from query to their postings lists.
     * @return postings list for the query <b>query</b>.
     */
    private PostingsList computeQuery(String query, Index termsMap){
        Set<String> operators = new TreeSet<String>();
        operators.add("AND");
        operators.add("OR");

        QueryMatcher qp = new QueryMatcher(operators, termsMap);
        return qp.matchQuery(query);
    }



    /**
     * Maps terms to their postings list by reading corresponding lines of the
     * index file.
     * @return map from term to its postings list.
     */
    private Index mapTerms(Set<String> termsSet) throws FileNotFoundException, IOException{
        Index termsMap = new Index();

        BufferedReader reader;
        reader = new BufferedReader(new FileReader(new File(indexLocation)));

        String textLine;
        boolean found = false;

        for(String token : termsSet){
            try{
                Term term = Term.newInstance(token);
                found = false;
                // locate term term in the index file and extract its postins list
                while((textLine = reader.readLine()) != null){
                    Term term1 = Term.fromIndexString(textLine);
                    if(term.equals(term1)){
                        termsMap.addTermPostingsListFromIndexString(textLine);
                        found = true;
                        break; // proceed with next term.
                    }
                }
                // This if fixes bug when legal term, that lexicographically precedes other terms
                // in termsSet, does not exist in the index and causes reader to process
                // entire index file looking for the term. After reader has reached the end of
                // file it cannot look for other terms any more. As a result, termsMap is returned empty.
                if(!found){
                    reader.close();
                    reader = new BufferedReader(new FileReader(new File(indexLocation)));
                }
            }catch(InvalidTermException skipThisToken){
                System.err.println("Invalid term ["+ token +"]");
                System.err.println("Skipping.");
            }
        }
        reader.close();
        return termsMap;
    }



    /**
     * Asks user whether he wants to enter new query.\
     * @return true if user wants to repeat search, false if he doesn't.
     */
    private boolean searchAgain(){
        String input = "";
        do{
            System.out.print("Search again? [y/n] > ");
            Scanner kbd = new Scanner(System.in);
            input = kbd.nextLine();
        }while(!input.equals("y") && !input.equals("n"));
        return input.equalsIgnoreCase("y");
    }



    /**
     * Reads query from standard input, echoes it back to user as a side effect.
     * @return user query.
     */
    private String getQuery(){
        Scanner kbd = new Scanner(System.in);
        String input = kbd.nextLine();
        System.out.println("Looking for " + input);
        return input;
    }



    /**
     * Displays welcome message and asks user for a query.
     */
    private void welcome(){
        System.out.println();
        System.out.print("What are you looking for? > ");
    }



    /**
     * Displays goodbye message.
     */
    private void bye(){
        System.out.println();
        System.out.println("Bye!");
        System.out.println();
    }



    private Searcher(String indexLocation, String cacheLocation){
        this.indexLocation = indexLocation;
        this.cacheLocation = cacheLocation;
    }



    private String indexLocation = "";
    private String cacheLocation = "";

    private final int MAX_DOCS = 10;
}
